﻿//
// Copyright (c) 2011 HyperCrunch, Inc. http://www.hypercrunch.com 
// Author: Nik Kalyani  http://www.kalyani.com (Twitter: @techbubble)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 
// documentation files (the "Software"), to deal in the Software without restriction, including without limitation 
// the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and 
// to permit persons to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or substantial portions 
// of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 
// TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 
// CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 
// DEALINGS IN THE SOFTWARE.
//

using System;
using System.Web;
using DotNetNuke.Entities.Portals;
using DotNetNuke.Common.Utilities;
using System.Collections.Generic;
using System.Web.Script.Serialization;
using System.Collections;
using DotNetNuke.Entities.Modules;
using DotNetNuke.UI.Skins;

namespace HyperCrunch.Druid
{
    public class RuleEngine
    {
        private RuleEngine()
        {
        }

        /// <summary>
        /// Modifies the standard response by switching the skin, container and/or theme as required by the rule
        /// Note that stylesheets and scripts are handled separately in the calling HttpModule via the ClientResourceManager
        /// </summary>
        /// <param name="uiContext"></param>
        private static void ModifyStandardResponse(UIContext uiContext)
        {
            RuleAction action = uiContext.Action;

            if (!String.IsNullOrEmpty(action.SkinControl))
                uiContext.ActiveTab.SkinSrc = Globals.ResolveUrl(uiContext, action.SkinControl);

            if (!String.IsNullOrEmpty(action.ContainerControl))
            {
                // Bug Workaround: DotNetNuke's Pane.cs ignores ActiveTab.ContainerSrc so we use this hack
                string uiContainer = SkinController.FormatSkinSrc(Globals.ResolveUrl(uiContext, action.ContainerControl), uiContext.PortalSettings);
                string pageContainer = uiContext.PortalSettings.ActiveTab.ContainerSrc.ToLower();
                ArrayList modules = uiContext.PortalSettings.ActiveTab.Modules;
                foreach (ModuleInfo module in modules)
                {
                    if (module.ContainerSrc.ToLower() == pageContainer)
                        module.ContainerSrc = uiContainer;
                }
            }

            if (!String.IsNullOrEmpty(action.Theme))
                uiContext.Context.Items.Add(Globals.CONTEXT_THEME, action.Theme); // Skinonym SuperSkin will pick this up and use
        }

        /// <summary>
        /// Rule enumeration calls for a custom response. This could be anything and has zero dependency on DNN's page rendering
        /// </summary>
        /// <param name="uiContext"></param>
        private static void RenderCustomResponse(UIContext uiContext)
        {
            HttpResponse response = uiContext.Context.Response;
            response.Clear();
            response.ClearHeaders();

            string rendererName = Globals.ResolveUrl(uiContext, uiContext.Action.ContentRenderer);
            if (rendererName.IndexOf(".") == -1)
                rendererName = "HyperCrunch.Druid.ContentRenderers." + rendererName + "ContentRenderer";

            Type rendererType = (Type)DataCache.GetCache(rendererName);
            if (rendererType == null)
            {
                rendererType = Type.GetType(rendererName);
                if (rendererType != null)
                    DataCache.SetCache(uiContext.Action.ContentRenderer, rendererType);
            }

            if (rendererType != null)
            {
                if (typeof(IContentRenderer).IsAssignableFrom(rendererType))
                {
                    IContentRenderer renderer = (IContentRenderer)Activator.CreateInstance(rendererType);
                    if (renderer != null)
                    {
                        ActionResult actionResult = renderer.RenderContent(uiContext);
                        actionResult.ExecuteResult(uiContext);
                    }
                }
            }
        }

        /// <summary>
        /// Main entry-point for rule processing. Reads and caches definitions and creates a response based on the rules
        /// </summary>
        /// <param name="uiContext"></param>
        public static RuleProcessingResultType Process(UIContext uiContext)
        {
            RuleProcessingResultType resultType = RuleProcessingResultType.ModifyStandardResponse;

            uiContext.Log("Starting rule processing...");
            try
            {
                HttpRequest request = uiContext.Context.Request;

                uiContext.Log("Reading host rules...");
                // Get the host level rule definition
                RuleDefinition definition = RuleDefinition.GetRuleDefinition(DotNetNuke.Common.Globals.HostMapPath + Globals.DRUID_CONFIG_FILE, "Druid.default", true);

                uiContext.Log("Merging portal rules...");
                // Merge in portal level rule definition if one exists
                definition = RuleDefinition.MergeRuleDefinition(definition, PortalSettings.Current.HomeDirectoryMapPath + Globals.DRUID_CONFIG_FILE, "Druid." + PortalSettings.Current.PortalId.ToString());

                // If there are any rules present, process them and determine actions based on matches
                if (definition.Rules != null)
                {
                    uiContext.Log("Begin rule processing...");
                    ProcessRules(definition.Rules, uiContext, "");
                    uiContext.Log("End rule processing...");
                }
                else
                    uiContext.Log("No rules found...");

                // Redirect actions are handled first since everything else is moot if a redirect is requested
                if (!String.IsNullOrEmpty(uiContext.Action.RedirectUrl))
                {
                    string[] frags = uiContext.Action.RedirectUrl.Split(';');
                    string url = Globals.ResolveUrl(uiContext, frags[0]);
                    int code = 301;

                    if (frags.Length > 1)
                        code = Convert.ToInt32(frags[1]);

                    HttpResponse response = uiContext.Context.Response;
                    response.StatusCode = code;

                    response.Redirect(url, true);
                    return RuleProcessingResultType.RedirectResponse;
                }


                if (!String.IsNullOrEmpty(uiContext.Action.ContentRenderer))
                {
                    RenderCustomResponse(uiContext);
                    resultType = RuleProcessingResultType.RenderCustomResponse;
                }
                else
                    ModifyStandardResponse(uiContext);
            }
            catch (Exception e)
            {
                uiContext.Log("ERROR: " + e.Message + " " + e.StackTrace);
            }

            return resultType;
        }

        private static void ProcessRules(RuleCollection rules, UIContext uiContext, string ruleLevel)
        {
            foreach (Rule r in rules)
            {
                try
                {

                    if (uiContext.Context.Request.IsAuthenticated)
                    {
                        if (r.IgnoreIf == IgnoreConditionType.Authenticated) continue;

                        if (r.IgnoreIf != IgnoreConditionType.Never)
                        {
                            if ((r.IgnoreIf == IgnoreConditionType.Superuser) && (uiContext.IsSuper)) continue;
                            if ((r.IgnoreIf == IgnoreConditionType.Administrator) && (uiContext.IsAdmin)) continue;
                        }
                    }

                    bool isMatch = false;
                    string matcher = r.MatchType;

                    if (r.MatchType.IndexOf(".") == -1)
                        matcher = "HyperCrunch.Druid.RuleMatchers." + matcher + "Matcher";

                    try
                    {
                        IRuleMatcher ruleMatcher = (IRuleMatcher)DataCache.GetCache(matcher);
                        if (ruleMatcher == null)
                        {
                            Type matcherType = Type.GetType(matcher);
                            if ((matcherType != null) && (typeof(IRuleMatcher).IsAssignableFrom(matcherType)))
                            {
                                ruleMatcher = (IRuleMatcher)Activator.CreateInstance(matcherType);
                                if (ruleMatcher != null)
                                    DataCache.SetCache(matcher, ruleMatcher);
                            }
                        }

                        if ((ruleMatcher != null) && ((!string.IsNullOrEmpty(r.MatchValues)) || r.MatchCondition == MatchConditionType.Exists))
                        {
                            isMatch = ruleMatcher.Match(uiContext, r.MatchField, r.MatchValues, r.MatchCondition);
                            uiContext.Log(new String('-', ruleLevel.Length / 2) + " RULE (" + ruleLevel + r.Order.ToString() + "): id=" + r.Id + ", type=" + matcher + " (" + r.MatchField + (String.IsNullOrEmpty(r.MatchField) ? "" : " => ") + r.MatchCondition.ToString() + " => " + r.MatchValues + ") RESULT: " + isMatch.ToString().ToUpper(), isMatch ? LogType.Match : LogType.NoMatch);
                        }
                        else
                            uiContext.Log(new String('-', ruleLevel.Length / 2) + " RULE (" + ruleLevel + r.Order.ToString() + "): id=" + r.Id + ", type=" + matcher + " (" + r.MatchField + (String.IsNullOrEmpty(r.MatchField) ? "" : " => ") + r.MatchCondition.ToString() + " => " + r.MatchValues + ") RESULT: Failed to create instance of " + matcher);
                    }
                    catch
                    {
                        // Rule processing is forgiving...if a matcher is not found, log it and continue
                        uiContext.Log("No match evaulator found for " + matcher); 
                    }

                    if (r.Debug != null)
                    {
                        try
                        {
                            uiContext.Debug = bool.Parse(r.Debug);
                        }
                        catch
                        {
                        }
                    }

                    if (isMatch)
                    {
                        if (!String.IsNullOrEmpty(r.SkinControl))
                            uiContext.Action.SkinControl = r.SkinControl;

                        if (!String.IsNullOrEmpty(r.ContainerControl))
                            uiContext.Action.ContainerControl = r.ContainerControl;

                        if (!String.IsNullOrEmpty(r.Theme))
                            uiContext.Action.Theme = r.Theme;

                        if (!String.IsNullOrEmpty(r.StylesheetUrl))
                            uiContext.StylesheetUrls.Add(r.StylesheetUrl);

                        if (!String.IsNullOrEmpty(r.ScriptUrl))
                            uiContext.ScriptUrls.Add(r.ScriptUrl);

                        if (!String.IsNullOrEmpty(r.RedirectUrl))
                            uiContext.Action.RedirectUrl = r.RedirectUrl;

                        if (!String.IsNullOrEmpty(r.ContentModifier))
                            uiContext.ContentModifiers.Add(r.ContentModifier);

                        if (!String.IsNullOrEmpty(r.ContentRenderer))
                            uiContext.Action.ContentRenderer = r.ContentRenderer;

                        if (!String.IsNullOrEmpty(r.JsonSettingName) && !String.IsNullOrEmpty(r.JsonSettingValue))
                        {
                            JavaScriptSerializer serializer = new JavaScriptSerializer();
                            string jsonValue = Globals.ReplaceTokens(r.JsonSettingValue, uiContext);
                            object jsonObject = serializer.DeserializeObject(jsonValue);
                            if (uiContext.JsonSettings.ContainsKey(r.JsonSettingName))
                                uiContext.JsonSettings[r.JsonSettingName] = jsonObject;
                            else
                                uiContext.JsonSettings.Add(r.JsonSettingName, jsonObject);
                        }

                        if (r.ContinueAfterMatch == ContinueAfterMatchType.Never) break;

                        // Recursive processing of hierarchical rule definitions
                        if (r.Rules != null)
                            ProcessRules(r.Rules, uiContext, ruleLevel + r.Order.ToString() + ".");
                    }
                }
                catch(Exception e)
                {
                    uiContext.Log("ERROR: " + r.MatchType + ". " + e.Message + " " + e.StackTrace);
                }
            }
        }
    }

}
